<?php

class Jf_Daemon {

    private $_pidFilePath = '/var/run/jf';

    private $_pidFileName = 'daemon.pid';

    private $_verbose = false;

    private $_singleton = true;

    private $_closeStdHandle = true;

    private $_pid = 0;

    private $_execFile = "";

    private $_signalHandlerFuns = array();

    public function __construct($configs = array()) {
        // load config
        if (is_array($configs))
            $this->setConfigs($configs);
    }

    public function _checkRequirement() {
        // check if pctnl loaded
        if (! extension_loaded('pcntl'))
            throw new Jf_Exception("daemon needs support of pcntl extension, please enable it.");
            // check sapi name,only for cli
        if ('cli' != php_sapi_name())
            throw new Jf_Exception("daemon only works in cli sapi.");
    }

    public function setConfigs($configs) {
        foreach ((array) $configs as $item => $config) {
            switch ($item) {
                case "pidFilePath":
                    $this->setPidFilePath($config);
                    break;
                case "pidFileName":
                    $this->setPidFileName($config);
                    break;
                case "verbose":
                    $this->setVerbose($config);
                    break;
                case "singleton":
                    $this->setSingleton($config);
                    break;
                case "closeStdHandle":
                    $this->setCloseStdHandle($config);
                    break;
                default:
                    throw new Jf_Exception("Unknown config item {$item}");
                    break;
            }
        }
    }

    public function setPidFilePath($path) {
        if (empty($path))
            return false;
        if (! is_dir($path))
            if (! mkdir($path, 0777))
                throw new Jf_Exception("setPidFilePath: cannnot make dir {$path}.");
        $this->_pidFilePath = rtrim($path, "/");
        return true;
    }

    public function getPidFilePath() {
        return $this->_pidFilePath;
    }

    public function setPidFileName($name) {
        if (empty($name))
            return false;
        $this->_pidFileName = trim($name);
        return true;
    }

    public function getPidFileName() {
        return $this->_pidFileName;
    }

    public function setVerbose($open = true) {
        $this->_verbose = (boolean) $open;
        return true;
    }

    public function getVerbose() {
        return $this->_verbose;
    }

    public function setSingleton($singleton = true) {
        $this->_singleton = (boolean) $singleton;
        return true;
    }

    public function getSingleton() {
        return $this->_singleton;
    }

    public function setCloseStdHandle($close = true) {
        $this->_closeStdHandle = (boolean) $close;
        return true;
    }

    public function getCloseStdHandle() {
        return $this->_closeStdHandle;
    }

    public function start() {
        $this->_checkRequirement();
        $this->_daemonize();
        // default handler for stop
        if (! pcntl_signal(SIGTERM, array(
            $this,
            "signalHandler"
        )))
            throw new Jf_Exception("Cannot setup signal handler for signo");
        if ($this->_closeStdHandle) {
            // fclose(STDIN);
            // fclose(STDOUT);
            // fclose(STDERR);
        }
        return true;
    }

    public function stop($force = false) {
        if ($force)
            $signo = SIGKILL; // kill -9
        else
            $signo = SIGTERM; // kill
            // only use in singleton model
        if (! $this->_singleton)
            throw new Jf_Exception("'stop' only use in singleton model.");
        if (false === ($pid = $this->_getPidFromFile()))
            throw new Jf_Exception("daemon is not running,cannot stop.");
        if (! posix_kill($pid, $signo)) {
            throw new Jf_Exception("Cannot send signal $signo to daemon.");
        }
        $this->_unlinkPidFile();
        $this->_out("Daemon stopped with pid {$pid}...");
        return true;
    }

    public function restart() {
        $this->stop();
        // sleep to wait
        sleep(1);
        $this->start();
    }

    public function getDaemonPid() {
        return $this->_getPidFromFile();
    }

    public function signalHandler($signo) {
        $signFuns = $this->_signalHandlerFuns[$signo];
        if (is_array($signFuns)) {
            foreach ($signFuns as $fun) {
                call_user_func($fun);
            }
        }
        // default action
        switch ($signo) {
            case SIGTERM:
                exit();
                break;
            default:
            // handle all other signals
        }
    }

    public function addSignalHandler($signo, $fun) {
        if (is_string($fun)) {
            if (! function_exists($fun)) {
                throw new Jf_Exception("handler function {$fun} not exists");
            }
        } elseif (is_array($fun)) {
            if (! @method_exists($fun[0], $fun[1])) {
                throw new Jf_Exception("handler method not exists");
            }
        } else {
            throw new Jf_Exception("error handler.");
        }
        if (! pcntl_signal($signo, array(
            $this,
            "signalHandler"
        )))
            throw new Jf_Exception("Cannot setup signal handler for signo {$signo}");
        $this->_signalHandlerFuns[$signo][] = $fun;
        return $this;
    }

    public function sendSignal($signo) {
        if (false === ($pid = $this->_getPidFromFile()))
            throw new Jf_Exception("daemon is not running,cannot send signal.");
        if (! posix_kill($pid, $signo)) {
            throw new Jf_Exception("Cannot send signal $signo to daemon.");
        }
        // $this->_out("Send signal $signo to pid $pid...");
        return true;
    }

    public function isActive() {
        try {
            $pid = $this->_getPidFromFile();
        } catch (Jf_Exception $e) {
            return false;
        }
        if (false === $pid)
            return false;
        if (false === ($active = @pcntl_getpriority($pid)))
            return false;
        else
            return true;
    }

    private function _daemonize() {
        // single model, first check if running
        if ($this->_singleton) {
            $isRunning = $this->_checkRunning();
            if ($isRunning)
                throw new Jf_Exception("Daemon already running");
        }
        // fork current process
        $pid = pcntl_fork();
        if ($pid == - 1) {
            // fork error
            throw new Jf_Exception("Error happened while fork process");
        } elseif ($pid) {
            // parent exit
            exit();
        } else {
            // child, get pid
            $this->_pid = posix_getpid();
        }
        $this->_out("Daemon started with pid {$this->_pid}...");
        // detach from controlling terminal
        if (! posix_setsid())
            throw new Jf_Exception("Cannot detach from terminal");
            // log pid in singleton model
        if ($this->_singleton)
            $this->_logPid();
        return $this->_pid;
    }

    private function _getPidFromFile() {
        // if is set
        if ($this->_pid)
            return (int) $this->_pid;
        $pidFile = $this->_pidFilePath . "/" . $this->_pidFileName;
        // no pid file,it's the first time of running
        if (! file_exists($pidFile))
            return false;
        if (! $handle = fopen($pidFile, "r"))
            throw new Jf_Exception("Cannot open pid file {$pidFile} for read");
        if (($pid = fread($handle, 1024)) === false)
            throw new Jf_Exception("Cannot read from pid file {$pidFile}");
        fclose($handle);
        return $this->_pid = (int) $pid;
    }

    private function _checkRunning() {
        $pid = $this->_getPidFromFile();
        // no pid file,not running
        if (false === $pid)
            return false;
            // get exe file path from pid
        switch (strtolower(PHP_OS)) {
            case "freebsd":
                $strExe = $this->_getFreebsdProcExe($pid);
                if ($strExe === false)
                    return false;
                $strArgs = $this->_getFreebsdProcArgs($pid);
                break;
            case "linux":
                $strExe = $this->_getLinuxProcExe($pid);
                if ($strExe === false)
                    return false;
                $strArgs = $this->_getLinuxProcArgs($pid);
                break;
            default:
                return false;
        }
        $exeRealPath = $this->_getDaemonRealPath($strArgs, $pid);
        // get exe file path from command
        if ($strExe != PHP_BINDIR . "/php")
            return false;
        $selfFile = "";
        $sapi = php_sapi_name();
        switch ($sapi) {
            case "cgi":
            case "cgi-fcgi":
                $selfFile = $_SERVER['argv'][0];
                break;
            default:
                $selfFile = $_SERVER['PHP_SELF'];
                break;
        }
        $currentRealPath = realpath($selfFile);
        // compare two path
        if ($currentRealPath != $exeRealPath)
            return false;
        else
            return true;
    }

    /**
     * log Pid
     */
    private function _logPid() {
        $pidFile = $this->_pidFilePath . "/" . $this->_pidFileName;
        if (! $handle = fopen($pidFile, "w")) {
            throw new Jf_Exception("Cannot open pid file {$pidFile} for write");
        }
        if (fwrite($handle, $this->_pid) == false) {
            throw new Jf_Exception("Cannot write to pid file {$pidFile}");
        }
        fclose($handle);
    }

    /**
     * unlink pid file
     * in singleton mode, unlink pid file while daemon stop
     * 
     * @return boolean
     */
    private function _unlinkPidFile() {
        $pidFile = $this->_pidFilePath . '/' . $this->_pidFileName;
        return @unlink($pidFile);
    }

    private function _getDaemonRealPath($daemonFile, $daemonPid) {
        $daemonFile = trim($daemonFile);
        if (substr($daemonFile, 0, 1) !== "/") {
            $cwd = $this->_getLinuxProcCwd($daemonPid);
            $cwd = rtrim($cwd, "/");
            $cwd = $cwd . "/" . $daemonFile;
            $cwd = realpath($cwd);
            return $cwd;
        }
        return realpath($daemonFile);
    }

    private function _getFreebsdProcExe($pid) {
        $strProcExeFile = "/proc/" . $pid . "/file";
        if (false === ($strLink = @readlink($strProcExeFile))) {
            // throw new Jf_Exception("Cannot read link file {$strProcExeFile}");
            return false;
        }
        return $strLink;
    }

    private function _getLinuxProcExe($pid) {
        $strProcExeFile = "/proc/" . $pid . "/exe";
        if (false === ($strLink = @readlink($strProcExeFile))) {
            // throw new Jf_Exception("Cannot read link file {$strProcExeFile}");
            return false;
        }
        return $strLink;
    }

    private function _getFreebsdProcArgs($pid) {
        return $this->_getLinuxProcArgs($pid);
    }

    private function _getLinuxProcArgs($pid) {
        $strProcCmdlineFile = "/proc/" . $pid . "/cmdline";
        if (! $fp = @fopen($strProcCmdlineFile, "r")) {
            throw new Jf_Exception("Cannot open file {$strProcCmdlineFile} for read");
        }
        if (! $strContents = fread($fp, 4096)) {
            throw new Jf_Exception("Cannot read or empty file {$strProcCmdlineFile}");
        }
        fclose($fp);
        $strContents = preg_replace("/[^/w/.///-]/", " ", trim($strContents));
        $strContents = preg_replace("//s+/", " ", $strContents);
        $arrTemp = explode(" ", $strContents);
        if (count($arrTemp) < 2) {
            throw new Jf_Exception("Invalid content in {$strProcCmdlineFile}");
        }
        return trim($arrTemp[1]);
    }

    private function _getLinuxProcCwd($pid) {
        $strProcExeFile = "/proc/" . $pid . "/cwd";
        if (false === ($strLink = @readlink($strProcExeFile))) {
            throw new Jf_Exception("Cannot read link file {$strProcExeFile}");
        }
        return $strLink;
    }

    private function _out($str) {
        if ($this->_verbose) {
            fwrite(STDOUT, $str . "/n");
        }
        return true;
    }
}  
      
   